/*
 * Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * The Universal Permissive License (UPL), Version 1.0
 *
 * Subject to the condition set forth below, permission is hereby granted to any
 * person obtaining a copy of this software, associated documentation and/or
 * data (collectively the "Software"), free of charge and under any and all
 * copyright rights in the Software, and any and all patent rights owned or
 * freely licensable by each licensor hereunder covering either (i) the
 * unmodified Software as contributed to or provided by such licensor, or (ii)
 * the Larger Works (as defined below), to deal in both
 *
 * (a) the Software, and
 *
 * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
 * one is included with the Software each a "Larger Work" to which the Software
 * is contributed by such licensors),
 *
 * without restriction, including without limitation the rights to copy, create
 * derivative works of, display, perform, and distribute the Software and make,
 * use, sell, offer for sale, import, export, have made, and have sold the
 * Software and the Larger Work(s), and to sublicense the foregoing rights on
 * either these or other terms.
 *
 * This license is subject to the following condition:
 *
 * The above copyright notice and either this complete permission notice or at a
 * minimum a reference to the UPL must be included in all copies or substantial
 * portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package org.graalvm.wasm;

import static org.graalvm.wasm.Assert.assertIntEqual;
import static org.graalvm.wasm.Assert.assertTrue;
import static org.graalvm.wasm.Assert.assertUnsignedIntLess;
import static org.graalvm.wasm.WasmMath.maxUnsigned;
import static org.graalvm.wasm.WasmMath.minUnsigned;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import com.oracle.truffle.api.nodes.ExplodeLoop;
import org.graalvm.collections.EconomicMap;
import org.graalvm.collections.EconomicSet;
import org.graalvm.collections.MapCursor;
import org.graalvm.wasm.api.Vector128;
import org.graalvm.wasm.constants.GlobalModifier;
import org.graalvm.wasm.constants.ImportIdentifier;
import org.graalvm.wasm.exception.Failure;
import org.graalvm.wasm.exception.WasmException;
import org.graalvm.wasm.exception.WasmRuntimeException;
import org.graalvm.wasm.memory.WasmMemory;
import org.graalvm.wasm.memory.WasmMemoryFactory;

import com.oracle.truffle.api.CompilerAsserts;
import com.oracle.truffle.api.CompilerDirectives;
import com.oracle.truffle.api.CompilerDirectives.CompilationFinal;

/**
 * Contains the symbol information of a module.
 */
public abstract class SymbolTable {
    private static final int INITIAL_GLOBALS_SIZE = 64;
    private static final int INITIAL_GLOBALS_BYTECODE_SIZE = INITIAL_GLOBALS_SIZE / 4;
    private static final int INITIAL_TABLE_SIZE = 1;
    private static final int INITIAL_MEMORY_SIZE = 1;
    private static final int INITIAL_DATA_SIZE = 512;
    private static final int INITIAL_TYPE_SIZE = 128;
    private static final int INITIAL_FUNCTION_TYPES_SIZE = 128;
    private static final int INITIAL_TAG_TYPE_SIZE = 1;
    private static final byte GLOBAL_MUTABLE_BIT = 0x01;
    private static final byte GLOBAL_EXPORTED_BIT = 0x02;
    private static final byte GLOBAL_INITIALIZED_BIT = 0x04;
    private static final byte GLOBAL_IMPORTED_BIT = 0x10;
    private static final byte GLOBAL_FUNCTION_INITIALIZER_BIT = 0x20;

    public static final int UNINITIALIZED_ADDRESS = Integer.MIN_VALUE;
    public static final int NO_EQUIVALENCE_CLASS = 0;
    public static final int FIRST_EQUIVALENCE_CLASS = NO_EQUIVALENCE_CLASS + 1;

    /**
     * Represents a WebAssembly value type in its closed form, with all type indices replaced with
     * their definitions. You can query the subtyping relation on types using the predicates
     * {@link #isSupertypeOf(ClosedValueType)} and {@link #isSubtypeOf(ClosedValueType)}, both of
     * which are written to be PE-friendly provided the receiver type is a PE constant.
     * <p>
     * If you need to check whether two types are equivalent, instead of checking
     * {@code A.isSupertypeOf(B) && A.isSubtypeOf(B)}, you can use {@code A.equals(B)}, since, in
     * the WebAssembly type system, type equivalence corresponds to structural equality.
     * </p>
     */
    public abstract static sealed class ClosedValueType {
        // This is a workaround until we can use pattern matching in JDK 21+.
        public enum Kind {
            Number,
            Vector,
            Reference
        }

        public abstract boolean isSupertypeOf(ClosedValueType valueSubType);

        public abstract boolean isSubtypeOf(ClosedValueType valueSuperType);

        public abstract boolean matchesValue(Object value);

        public abstract Kind kind();
    }

    public abstract static sealed class ClosedHeapType {
        // This is a workaround until we can use pattern matching in JDK 21+.
        public enum Kind {
            Abstract,
            Function
        }

        public abstract boolean isSupertypeOf(ClosedHeapType heapSubType);

        public abstract boolean isSubtypeOf(ClosedHeapType heapSuperType);

        public abstract boolean matchesValue(Object value);

        public abstract Kind kind();
    }

    public static final class NumberType extends ClosedValueType {
        public static final NumberType I32 = new NumberType(WasmType.I32_TYPE);
        public static final NumberType I64 = new NumberType(WasmType.I64_TYPE);
        public static final NumberType F32 = new NumberType(WasmType.F32_TYPE);
        public static final NumberType F64 = new NumberType(WasmType.F64_TYPE);

        private final int value;

        private NumberType(int value) {
            this.value = value;
        }

        public int value() {
            return value;
        }

        @Override
        public boolean isSupertypeOf(ClosedValueType valueSubType) {
            return valueSubType == this;
        }

        @Override
        public boolean isSubtypeOf(ClosedValueType valueSuperType) {
            return valueSuperType == this;
        }

        @Override
        public boolean matchesValue(Object val) {
            return switch (value()) {
                case WasmType.I32_TYPE -> val instanceof Integer;
                case WasmType.I64_TYPE -> val instanceof Long;
                case WasmType.F32_TYPE -> val instanceof Float;
                case WasmType.F64_TYPE -> val instanceof Double;
                default -> throw CompilerDirectives.shouldNotReachHere();
            };
        }

        @Override
        public Kind kind() {
            return Kind.Number;
        }

        @Override
        public boolean equals(Object that) {
            return this == that;
        }

        @Override
        public int hashCode() {
            return value;
        }

        @Override
        public String toString() {
            return switch (value()) {
                case WasmType.I32_TYPE -> "i32";
                case WasmType.I64_TYPE -> "i64";
                case WasmType.F32_TYPE -> "f32";
                case WasmType.F64_TYPE -> "f64";
                default -> throw CompilerDirectives.shouldNotReachHere();
            };
        }
    }

    public static final class VectorType extends ClosedValueType {
        public static final VectorType V128 = new VectorType(WasmType.V128_TYPE);

        private final int value;

        private VectorType(int value) {
            this.value = value;
        }

        public int value() {
            return value;
        }

        @Override
        public boolean isSupertypeOf(ClosedValueType valueSubType) {
            return valueSubType == V128;
        }

        @Override
        public boolean isSubtypeOf(ClosedValueType valueSuperType) {
            return valueSuperType == V128;
        }

        @Override
        public boolean matchesValue(Object val) {
            return val instanceof Vector128;
        }

        @Override
        public Kind kind() {
            return Kind.Vector;
        }

        @Override
        public boolean equals(Object that) {
            return this == that;
        }

        @Override
        public int hashCode() {
            return value;
        }

        @Override
        public String toString() {
            return "v128";
        }
    }

    public static final class ClosedReferenceType extends ClosedValueType {
        public static final ClosedReferenceType FUNCREF = new ClosedReferenceType(true, AbstractHeapType.FUNC);
        public static final ClosedReferenceType NONNULL_FUNCREF = new ClosedReferenceType(false, AbstractHeapType.FUNC);
        public static final ClosedReferenceType EXTERNREF = new ClosedReferenceType(true, AbstractHeapType.EXTERN);
        public static final ClosedReferenceType NONNULL_EXTERNREF = new ClosedReferenceType(false, AbstractHeapType.EXTERN);
        public static final ClosedReferenceType EXNREF = new ClosedReferenceType(true, AbstractHeapType.EXN);
        public static final ClosedReferenceType NONNULL_EXNREF = new ClosedReferenceType(false, AbstractHeapType.EXN);

        private final boolean nullable;
        private final ClosedHeapType closedHeapType;

        public ClosedReferenceType(boolean nullable, ClosedHeapType closedHeapType) {
            this.nullable = nullable;
            this.closedHeapType = closedHeapType;
        }

        public boolean nullable() {
            return nullable;
        }

        public ClosedHeapType heapType() {
            return closedHeapType;
        }

        @Override
        public boolean isSupertypeOf(ClosedValueType valueSubType) {
            return valueSubType instanceof ClosedReferenceType referenceSubType && (!referenceSubType.nullable || this.nullable) &&
                            this.closedHeapType.isSupertypeOf(referenceSubType.closedHeapType);
        }

        @Override
        public boolean isSubtypeOf(ClosedValueType valueSuperType) {
            return valueSuperType instanceof ClosedReferenceType referencedSuperType && (!this.nullable || referencedSuperType.nullable) &&
                            this.closedHeapType.isSubtypeOf(referencedSuperType.closedHeapType);
        }

        @Override
        public boolean matchesValue(Object value) {
            return nullable() && value == WasmConstant.NULL || heapType().matchesValue(value);
        }

        @Override
        public Kind kind() {
            return Kind.Reference;
        }

        @Override
        public boolean equals(Object obj) {
            return obj instanceof ClosedReferenceType that && this.nullable == that.nullable && this.closedHeapType.equals(that.closedHeapType);
        }

        @Override
        public int hashCode() {
            return Boolean.hashCode(nullable) ^ closedHeapType.hashCode();
        }

        @Override
        public String toString() {
            CompilerAsserts.neverPartOfCompilation();
            if (this == FUNCREF) {
                return "funcref";
            } else if (this == EXTERNREF) {
                return "externref";
            } else if (this == EXNREF) {
                return "exnref";
            } else {
                StringBuilder buf = new StringBuilder();
                buf.append("(ref ");
                if (nullable) {
                    buf.append("null ");
                }
                buf.append(closedHeapType.toString());
                buf.append(")");
                return buf.toString();
            }
        }
    }

    public static final class AbstractHeapType extends ClosedHeapType {
        public static final AbstractHeapType FUNC = new AbstractHeapType(WasmType.FUNC_HEAPTYPE);
        public static final AbstractHeapType EXTERN = new AbstractHeapType(WasmType.EXTERN_HEAPTYPE);
        public static final AbstractHeapType EXN = new AbstractHeapType(WasmType.EXN_HEAPTYPE);

        private final int value;

        private AbstractHeapType(int value) {
            this.value = value;
        }

        public int value() {
            return value;
        }

        @Override
        public boolean isSupertypeOf(ClosedHeapType heapSubType) {
            return switch (this.value) {
                case WasmType.FUNC_HEAPTYPE -> heapSubType == FUNC || heapSubType instanceof ClosedFunctionType;
                case WasmType.EXTERN_HEAPTYPE -> heapSubType == EXTERN;
                case WasmType.EXN_HEAPTYPE -> heapSubType == EXN;
                default -> throw CompilerDirectives.shouldNotReachHere();
            };
        }

        @Override
        public boolean isSubtypeOf(ClosedHeapType heapSuperType) {
            return heapSuperType == this;
        }

        @Override
        public boolean matchesValue(Object val) {
            return switch (this.value) {
                case WasmType.FUNC_HEAPTYPE -> val instanceof WasmFunctionInstance;
                case WasmType.EXTERN_HEAPTYPE -> true;
                case WasmType.EXN_HEAPTYPE -> val instanceof WasmRuntimeException;
                default -> throw CompilerDirectives.shouldNotReachHere();
            };
        }

        @Override
        public Kind kind() {
            return Kind.Abstract;
        }

        @Override
        public boolean equals(Object that) {
            return this == that;
        }

        @Override
        public int hashCode() {
            return value;
        }

        @Override
        public String toString() {
            return switch (this.value) {
                case WasmType.FUNC_HEAPTYPE -> "func";
                case WasmType.EXTERN_HEAPTYPE -> "extern";
                case WasmType.EXN_HEAPTYPE -> "exn";
                default -> throw CompilerDirectives.shouldNotReachHere();
            };
        }
    }

    public static final class ClosedFunctionType extends ClosedHeapType {
        @CompilationFinal(dimensions = 1) private final ClosedValueType[] paramTypes;
        @CompilationFinal(dimensions = 1) private final ClosedValueType[] resultTypes;

        public ClosedFunctionType(ClosedValueType[] paramTypes, ClosedValueType[] resultTypes) {
            this.paramTypes = paramTypes;
            this.resultTypes = resultTypes;
        }

        public ClosedValueType[] paramTypes() {
            return paramTypes;
        }

        public ClosedValueType[] resultTypes() {
            return resultTypes;
        }

        @Override
        @ExplodeLoop(kind = ExplodeLoop.LoopExplosionKind.FULL_UNROLL)
        public boolean isSupertypeOf(ClosedHeapType heapSubType) {
            if (!(heapSubType instanceof ClosedFunctionType functionSubType)) {
                return false;
            }
            if (this.paramTypes.length != functionSubType.paramTypes.length) {
                return false;
            }
            for (int i = 0; i < this.paramTypes.length; i++) {
                CompilerAsserts.partialEvaluationConstant(this.paramTypes[i]);
                if (!this.paramTypes[i].isSubtypeOf(functionSubType.paramTypes[i])) {
                    return false;
                }
            }
            if (this.resultTypes.length != functionSubType.resultTypes.length) {
                return false;
            }
            for (int i = 0; i < this.resultTypes.length; i++) {
                CompilerAsserts.partialEvaluationConstant(this.resultTypes[i]);
                if (!this.resultTypes[i].isSupertypeOf(functionSubType.resultTypes[i])) {
                    return false;
                }
            }
            return true;
        }

        @Override
        @ExplodeLoop(kind = ExplodeLoop.LoopExplosionKind.FULL_UNROLL)
        public boolean isSubtypeOf(ClosedHeapType heapSuperType) {
            if (heapSuperType == AbstractHeapType.FUNC) {
                return true;
            }
            if (!(heapSuperType instanceof ClosedFunctionType functionSuperType)) {
                return false;
            }
            if (this.paramTypes.length != functionSuperType.paramTypes.length) {
                return false;
            }
            for (int i = 0; i < this.paramTypes.length; i++) {
                CompilerAsserts.partialEvaluationConstant(this.paramTypes[i]);
                if (!this.paramTypes[i].isSupertypeOf(functionSuperType.paramTypes[i])) {
                    return false;
                }
            }
            if (this.resultTypes.length != functionSuperType.resultTypes.length) {
                return false;
            }
            for (int i = 0; i < this.resultTypes.length; i++) {
                CompilerAsserts.partialEvaluationConstant(this.resultTypes[i]);
                if (!this.resultTypes[i].isSubtypeOf(functionSuperType.resultTypes[i])) {
                    return false;
                }
            }
            return true;
        }

        @Override
        public boolean matchesValue(Object value) {
            return value instanceof WasmFunctionInstance instance && isSupertypeOf(instance.function().closedType());
        }

        @Override
        public Kind kind() {
            return Kind.Function;
        }

        @Override
        public boolean equals(Object obj) {
            return obj instanceof ClosedFunctionType that && Arrays.equals(this.paramTypes, that.paramTypes) && Arrays.equals(this.resultTypes, that.resultTypes);
        }

        @Override
        public int hashCode() {
            return Arrays.hashCode(paramTypes) ^ Arrays.hashCode(resultTypes);
        }

        @Override
        public String toString() {
            CompilerAsserts.neverPartOfCompilation();
            String[] paramNames = new String[paramTypes.length];
            for (int i = 0; i < paramTypes.length; i++) {
                paramNames[i] = paramTypes[i].toString();
            }
            String[] resultNames = new String[resultTypes.length];
            for (int i = 0; i < resultTypes.length; i++) {
                resultNames[i] = resultTypes[i].toString();
            }
            return "(" + String.join(" ", paramNames) + ")->(" + String.join(" ", resultNames) + ")";
        }
    }

    /**
     * @param initialSize Lower bound on table size.
     * @param maximumSize Upper bound on table size.
     *            <p>
     *            <em>Note:</em> this is the upper bound defined by the module. A table instance
     *            might have a lower internal max allowed size in practice.
     * @param elemType The element type of the table.
     * @param initValue The initial value of the table's elements, can be {@code null} if no
     *            initializer present
     * @param initBytecode The bytecode of the table's initializer expression, can be {@code null}
     *            if no initializer present
     */
    public record TableInfo(int initialSize, int maximumSize, int elemType, Object initValue, byte[] initBytecode) {
    }

    /**
     * @param initialSize Lower bound on memory size (in pages of 64 kiB).
     * @param maximumSize Upper bound on memory size (in pages of 64 kiB).
     *            <p>
     *            <em>Note:</em> this is the upper bound defined by the module. A memory instance
     *            might have a lower internal max allowed size in practice.
     * @param indexType64 If the memory uses index type 64.
     * @param shared Whether the memory is shared (modifications are visible to other threads).
     */
    public record MemoryInfo(long initialSize, long maximumSize, boolean indexType64, boolean shared) {
    }

    /**
     * @param attribute Attribute of the tag.
     * @param typeIndex The type index of the tag.
     */
    public record TagInfo(byte attribute, int typeIndex) {
    }

    /**
     * Encodes the parameter and result types of each function type.
     * <p>
     * Given a function type index, the {@link #typeOffsets} array indicates where the encoding for
     * that function type begins in this array.
     * <p>
     * For a function type starting at index i, the encoding is the following
     * <p>
     * <code>
     *   i     i+1   i+2+0        i+2+na-1  i+2+na+0        i+2+na+nr-1
     * +-----+-----+-------+-----+--------+----------+-----+-----------+
     * | na  |  nr | par 1 | ... | par na | result 1 | ... | result nr |
     * +-----+-----+-------+-----+--------+----------+-----+-----------+
     * </code>
     * <p>
     * where `na` is the number of parameters, and `nr` is the number of result values.
     * <p>
     * This array is monotonically populated from left to right during parsing. Any code that uses
     * this array should only access the locations in the array that have already been populated.
     */
    @CompilationFinal(dimensions = 1) private int[] typeData;

    /**
     * Stores the offset of each function type into the {@link #typeData} array.
     * <p>
     * This array is monotonically populated from left to right during parsing. Any code that uses
     * this array should only access the locations in the array that have already been populated.
     */
    @CompilationFinal(dimensions = 1) private int[] typeOffsets;

    /**
     * Stores the closed forms of all the types defined in this module. Closed forms replace type
     * indices with the definitions of the referenced types, resulting in a tree-like data
     * structure.
     */
    @CompilationFinal(dimensions = 1) private ClosedHeapType[] closedTypes;

    /**
     * Stores the type equivalence class.
     * <p>
     * Since multiple types have the same shape, each type is mapped to an equivalence class, so
     * that two types can be quickly compared.
     * <p>
     * The equivalence classes are computed globally for all the modules, during linking.
     */
    @CompilationFinal(dimensions = 1) private int[] typeEquivalenceClasses;

    @CompilationFinal private int typeDataSize;
    @CompilationFinal private int typeCount;

    /**
     * List of the descriptors of all the imported symbols.
     */
    private final List<ImportDescriptor> importedSymbols;

    /**
     * List of the names of all the exported symbols.
     */
    private final List<String> exportedSymbols;

    /**
     * Stores the function objects for a WebAssembly module.
     * <p>
     * This array is monotonically populated from left to right during parsing. Any code that uses
     * this array should only access the locations in the array that have already been populated.
     */
    @CompilationFinal(dimensions = 1) private WasmFunction[] functions;
    @CompilationFinal private int numFunctions;

    /**
     * List of all imported functions.
     */
    private final List<WasmFunction> importedFunctions;

    @CompilationFinal private int numImportedFunctions;

    /**
     * Map from exported function names to respective functions.
     */
    private final EconomicMap<String, WasmFunction> exportedFunctions;

    /**
     * Map from function indices to the exported names of respective functions.
     */
    private final EconomicMap<Integer, String> exportedFunctionsByIndex;

    /**
     * Index of the start function if it exists, or -1 otherwise.
     */
    @CompilationFinal private int startFunctionIndex;

    /**
     * Value types of globals.
     */
    @CompilationFinal(dimensions = 1) private int[] globalTypes;

    /**
     * Mutability flags of globals. These are encoded like this:
     * <p>
     * <code>
     * | . | . | . | functionOrIndex flag | reference flag | initialized flag | exported flag | mutable flag |
     * </code>
     */
    @CompilationFinal(dimensions = 1) private byte[] globalFlags;

    /**
     * The values or indices used for initializing globals.
     */
    @CompilationFinal(dimensions = 1) private Object[] globalInitializers;

    /**
     * The bytecodes used for initializing globals.
     */
    @CompilationFinal(dimensions = 2) private byte[][] globalInitializersBytecode;

    /**
     * A mapping between the indices of the imported globals and their import specifiers.
     */
    @CompilationFinal private final EconomicMap<Integer, ImportDescriptor> importedGlobals;

    /**
     * A mapping between the names and the indices of the exported globals.
     */
    @CompilationFinal private final EconomicMap<String, Integer> exportedGlobals;

    /**
     * Number of globals in the module.
     */
    @CompilationFinal private int numGlobals;
    /**
     * Number of "external" (imported or exported) globals in the module.
     */
    @CompilationFinal private int numExternalGlobals;

    /**
     * A mapping between the indices of the globals and their corresponding global instance slot.
     * Negative addresses point into the external globals array, where {@code index = -address-1},
     * while 0 and positive addresses point into the internal globals array.
     * <p>
     * This array is monotonically populated from the left. Index i denotes the i-th global in this
     * module. The value at index i denotes the address of the global in the memory space for all
     * the globals of this module (see {@link GlobalRegistry}).
     * <p>
     * This mapping of global indices is done because, while the index and address spaces of the
     * globals are module-specific, the address space of the globals is split up into two regions
     * based on whether they have internal (local) or external (imported and/or exported) linkage.
     * <p>
     * Global addresses are assigned after the symbol table is fully parsed.
     *
     * @see #finishSymbolTable()
     */
    @CompilationFinal(dimensions = 1) private int[] globalAddresses;

    /**
     * Number of globals that need a bytecode initializer.
     */
    @CompilationFinal private int numGlobalInitializersBytecode;

    /**
     * The descriptor of the tables of this module.
     * <p>
     * In the current WebAssembly specification, a module can use at most one table. The value
     * {@code null} denotes that this module uses no table.
     */
    @CompilationFinal(dimensions = 1) private TableInfo[] tables;

    @CompilationFinal private int tableCount;

    /**
     * The table used in this module.
     */
    @CompilationFinal private final EconomicMap<Integer, ImportDescriptor> importedTables;

    /**
     * The name(s) of the exported table of this module, if any.
     */
    @CompilationFinal private final EconomicMap<String, Integer> exportedTables;

    /**
     * The descriptors of the memories of this module.
     */
    @CompilationFinal(dimensions = 1) private MemoryInfo[] memories;

    @CompilationFinal private int memoryCount;

    /**
     * The memories used in this module.
     */
    @CompilationFinal private final EconomicMap<Integer, ImportDescriptor> importedMemories;

    /**
     * The name(s) of the exported memories of this module, if any.
     */
    @CompilationFinal private final EconomicMap<String, Integer> exportedMemories;

    /**
     * The descriptors of the tags of this module.
     */
    @CompilationFinal(dimensions = 1) private TagInfo[] tags;

    @CompilationFinal private int tagCount;

    /**
     * The tags used in this module.
     */
    @CompilationFinal private final EconomicMap<Integer, ImportDescriptor> importedTags;

    /**
     * The name(s) of the exported tags of this module, if any.
     */
    @CompilationFinal private final EconomicMap<String, Integer> exportedTags;

    /**
     * List of all custom sections.
     */
    private final List<WasmCustomSection> customSections;

    @CompilationFinal private int elemSegmentCount;

    /**
     * The offsets of the data instances in the bytecode.
     */
    @CompilationFinal(dimensions = 1) private int[] dataInstances;

    /**
     * The offsets of the elem instances in the bytecode.
     */
    @CompilationFinal(dimensions = 1) private long[] elemInstances;

    /**
     * The offset of the code entries in the bytecode.
     */
    @CompilationFinal(dimensions = 1) private int[] codeEntries;
    @CompilationFinal private boolean dataCountExists;
    @CompilationFinal private int dataSegmentCount;

    /**
     * Offset representing dropped data instances.
     */
    @CompilationFinal private int droppedDataInstanceOffset;

    @CompilationFinal private int codeEntryCount;

    /**
     * All function indices that can be referenced via
     * {@link org.graalvm.wasm.constants.Instructions#REF_FUNC}.
     */
    @CompilationFinal private EconomicSet<Integer> functionReferences;

    SymbolTable() {
        CompilerAsserts.neverPartOfCompilation();
        this.typeData = new int[INITIAL_DATA_SIZE];
        this.typeOffsets = new int[INITIAL_TYPE_SIZE];
        this.closedTypes = new ClosedHeapType[INITIAL_TYPE_SIZE];
        this.typeEquivalenceClasses = new int[INITIAL_TYPE_SIZE];
        this.typeDataSize = 0;
        this.typeCount = 0;
        this.importedSymbols = new ArrayList<>();
        this.exportedSymbols = new ArrayList<>();
        this.functions = new WasmFunction[INITIAL_FUNCTION_TYPES_SIZE];
        this.numFunctions = 0;
        this.importedFunctions = new ArrayList<>();
        this.numImportedFunctions = 0;
        this.exportedFunctions = EconomicMap.create();
        this.exportedFunctionsByIndex = EconomicMap.create();
        this.startFunctionIndex = -1;
        this.globalTypes = new int[INITIAL_GLOBALS_SIZE];
        this.globalFlags = new byte[INITIAL_GLOBALS_SIZE];
        this.globalInitializers = new Object[INITIAL_GLOBALS_SIZE];
        this.globalInitializersBytecode = new byte[INITIAL_GLOBALS_BYTECODE_SIZE][];
        this.importedGlobals = EconomicMap.create();
        this.exportedGlobals = EconomicMap.create();
        this.numGlobals = 0;
        this.tables = new TableInfo[INITIAL_TABLE_SIZE];
        this.tableCount = 0;
        this.importedTables = EconomicMap.create();
        this.exportedTables = EconomicMap.create();
        this.memories = new MemoryInfo[INITIAL_MEMORY_SIZE];
        this.memoryCount = 0;
        this.importedMemories = EconomicMap.create();
        this.exportedMemories = EconomicMap.create();
        this.tags = new TagInfo[INITIAL_TAG_TYPE_SIZE];
        this.importedTags = EconomicMap.create();
        this.exportedTags = EconomicMap.create();
        this.customSections = new ArrayList<>();
        this.elemSegmentCount = 0;
        this.dataCountExists = false;
        this.dataSegmentCount = 0;
        this.functionReferences = EconomicSet.create();
        this.dataInstances = null;
    }

    private void checkNotParsed() {
        CompilerAsserts.neverPartOfCompilation();
        // The symbol table must be read-only after the module gets linked.
        if (module().isParsed()) {
            throw CompilerDirectives.shouldNotReachHere("The engine tried to modify the symbol table after parsing.");
        }
    }

    private void checkUniqueExport(String name) {
        CompilerAsserts.neverPartOfCompilation();
        if (exportedFunctions.containsKey(name) || exportedGlobals.containsKey(name) || exportedMemories.containsKey(name) || exportedTables.containsKey(name) || exportedTags.containsKey(name)) {
            throw WasmException.create(Failure.DUPLICATE_EXPORT, "All export names must be different, but '" + name + "' is exported twice.");
        }
    }

    public void checkFunctionIndex(int funcIndex) {
        assertUnsignedIntLess(funcIndex, numFunctions, Failure.UNKNOWN_FUNCTION);
    }

    /**
     * Ensure that the {@link #typeData} array has enough space to store {@code index}. If there is
     * no enough space, then a reallocation of the array takes place, doubling its capacity.
     * <p>
     * No synchronisation is required for this method, as it is only called during parsing, which is
     * carried out by a single thread.
     */
    private void ensureTypeDataCapacity(int index) {
        if (typeData.length <= index) {
            int newLength = Math.max(Integer.highestOneBit(index) << 1, 2 * typeData.length);
            typeData = Arrays.copyOf(typeData, newLength);
        }
    }

    /**
     * Ensure that the {@link #typeOffsets} and {@link #typeEquivalenceClasses} arrays have enough
     * space to store the data for the type at {@code index}. If there is not enough space, then a
     * reallocation of the array takes place, doubling its capacity.
     * <p>
     * No synchronisation is required for this method, as it is only called during parsing, which is
     * carried out by a single thread.
     */
    private void ensureTypeCapacity(int index) {
        if (typeOffsets.length <= index) {
            int newLength = Math.max(Integer.highestOneBit(index) << 1, 2 * typeOffsets.length);
            typeOffsets = Arrays.copyOf(typeOffsets, newLength);
            closedTypes = Arrays.copyOf(closedTypes, newLength);
            typeEquivalenceClasses = Arrays.copyOf(typeEquivalenceClasses, newLength);
        }
    }

    int allocateFunctionType(int paramCount, int resultCount, boolean isMultiValue) {
        checkNotParsed();
        ensureTypeCapacity(typeCount);
        int typeIdx = typeCount++;
        typeOffsets[typeIdx] = typeDataSize;

        if (!isMultiValue && resultCount != 0 && resultCount != 1) {
            throw WasmException.create(Failure.INVALID_RESULT_ARITY, "A function can return at most one result.");
        }

        int size = 2 + paramCount + resultCount;
        ensureTypeDataCapacity(typeDataSize + size);
        typeData[typeDataSize + 0] = paramCount;
        typeData[typeDataSize + 1] = resultCount;
        typeDataSize += size;
        return typeIdx;
    }

    public int allocateFunctionType(int[] paramTypes, int[] resultTypes, boolean isMultiValue) {
        checkNotParsed();
        final int typeIdx = allocateFunctionType(paramTypes.length, resultTypes.length, isMultiValue);
        for (int i = 0; i < paramTypes.length; i++) {
            registerFunctionTypeParameterType(typeIdx, i, paramTypes[i]);
        }
        for (int i = 0; i < resultTypes.length; i++) {
            registerFunctionTypeResultType(typeIdx, i, resultTypes[i]);
        }
        finishFunctionType(typeIdx);
        return typeIdx;
    }

    void registerFunctionTypeParameterType(int funcTypeIdx, int paramIdx, int type) {
        checkNotParsed();
        int idx = 2 + typeOffsets[funcTypeIdx] + paramIdx;
        typeData[idx] = type;
    }

    void registerFunctionTypeResultType(int funcTypeIdx, int resultIdx, int type) {
        checkNotParsed();
        int idx = 2 + typeOffsets[funcTypeIdx] + typeData[typeOffsets[funcTypeIdx]] + resultIdx;
        typeData[idx] = type;
    }

    void finishFunctionType(int funcTypeIdx) {
        ClosedValueType[] paramTypes = new ClosedValueType[functionTypeParamCount(funcTypeIdx)];
        for (int i = 0; i < paramTypes.length; i++) {
            paramTypes[i] = closedTypeOf(functionTypeParamTypeAt(funcTypeIdx, i));
        }
        ClosedValueType[] resultTypes = new ClosedValueType[functionTypeResultCount(funcTypeIdx)];
        for (int i = 0; i < resultTypes.length; i++) {
            resultTypes[i] = closedTypeOf(functionTypeResultTypeAt(funcTypeIdx, i));
        }
        closedTypes[funcTypeIdx] = new ClosedFunctionType(paramTypes, resultTypes);
    }

    public int equivalenceClass(int typeIndex) {
        return typeEquivalenceClasses[typeIndex];
    }

    void setEquivalenceClass(int index, int eqClass) {
        checkNotParsed();
        if (typeEquivalenceClasses[index] != NO_EQUIVALENCE_CLASS) {
            throw WasmException.create(Failure.UNSPECIFIED_INVALID, "Type at index " + index + " already has an equivalence class.");
        }
        typeEquivalenceClasses[index] = eqClass;
    }

    private void ensureFunctionsCapacity(int index) {
        if (functions.length <= index) {
            int newLength = Math.max(Integer.highestOneBit(index) << 1, 2 * functions.length);
            functions = Arrays.copyOf(functions, newLength);
        }
    }

    private WasmFunction allocateFunction(int typeIndex, ImportDescriptor importDescriptor) {
        checkNotParsed();
        ensureFunctionsCapacity(numFunctions);
        assertUnsignedIntLess(typeIndex, typeCount(), Failure.UNKNOWN_TYPE);
        final WasmFunction function = new WasmFunction(this, numFunctions, typeIndex, importDescriptor);
        functions[numFunctions] = function;
        numFunctions++;
        return function;
    }

    public WasmFunction declareFunction(int typeIndex) {
        checkNotParsed();
        return allocateFunction(typeIndex, null);
    }

    public WasmFunction declareExportedFunction(int typeIndex, String exportedName) {
        checkNotParsed();
        final WasmFunction function = declareFunction(typeIndex);
        exportFunction(function.index(), exportedName);
        return function;
    }

    String exportedFunctionName(int index) {
        return exportedFunctionsByIndex.get(index);
    }

    void setStartFunction(int functionIndex) {
        checkNotParsed();
        WasmFunction start = function(functionIndex);
        if (start.paramCount() != 0) {
            throw WasmException.create(Failure.START_FUNCTION_PARAMS, "Start function cannot take parameters.");
        }
        if (start.resultCount() != 0) {
            throw WasmException.create(Failure.START_FUNCTION_RESULT_VALUE, "Start function cannot return a value.");
        }
        this.startFunctionIndex = functionIndex;
    }

    public int numFunctions() {
        return numFunctions;
    }

    public WasmFunction function(int funcIndex) {
        assert 0 <= funcIndex && funcIndex <= numFunctions() - 1;
        return functions[funcIndex];
    }

    public WasmFunction function(String exportName) {
        return exportedFunctions.get(exportName);
    }

    public int functionTypeParamCount(int typeIndex) {
        int typeOffset = typeOffsets[typeIndex];
        return typeData[typeOffset + 0];
    }

    public int functionTypeResultCount(int typeIndex) {
        int typeOffset = typeOffsets[typeIndex];
        return typeData[typeOffset + 1];
    }

    public WasmFunction startFunction() {
        if (startFunctionIndex == -1) {
            return null;
        }
        return function(startFunctionIndex);
    }

    protected abstract WasmModule module();

    public int functionTypeParamTypeAt(int typeIndex, int paramIndex) {
        int typeOffset = typeOffsets[typeIndex];
        return typeData[typeOffset + 2 + paramIndex];
    }

    public int functionTypeResultTypeAt(int typeIndex, int resultIndex) {
        int typeOffset = typeOffsets[typeIndex];
        int paramCount = typeData[typeOffset];
        return typeData[typeOffset + 2 + paramCount + resultIndex];
    }

    public int[] functionTypeParamTypesAsArray(int typeIndex) {
        int paramCount = functionTypeParamCount(typeIndex);
        int[] paramTypes = new int[paramCount];
        for (int i = 0; i < paramCount; ++i) {
            paramTypes[i] = functionTypeParamTypeAt(typeIndex, i);
        }
        return paramTypes;
    }

    public int[] functionTypeResultTypesAsArray(int typeIndex) {
        int resultTypeCount = functionTypeResultCount(typeIndex);
        int[] resultTypes = new int[resultTypeCount];
        for (int i = 0; i < resultTypeCount; i++) {
            resultTypes[i] = functionTypeResultTypeAt(typeIndex, i);
        }
        return resultTypes;
    }

    int typeCount() {
        return typeCount;
    }

    /**
     * Convenience function for calling {@link #closedTypeAt(int)} when the defined type at index
     * {@code typeIndex} is known to be a function type.
     * 
     * @see #closedTypeAt(int)
     */
    public ClosedFunctionType closedFunctionTypeAt(int typeIndex) {
        return (ClosedFunctionType) closedTypeAt(typeIndex);
    }

    /**
     * Fetches the closed form of a type defined in this module at index {@code typeIndex}.
     * 
     * @param typeIndex index of a type defined in this module
     */
    public ClosedHeapType closedTypeAt(int typeIndex) {
        return closedTypes[typeIndex];
    }

    /**
     * A convenient way of calling {@link #closedTypeOf(int, SymbolTable)} when a
     * {@link SymbolTable} is present.
     * 
     * @see #closedTypeOf(int, SymbolTable)
     */
    public ClosedValueType closedTypeOf(int type) {
        return SymbolTable.closedTypeOf(type, this);
    }

    /**
     * Maps a type encoded as an {@code int} (as per {@link WasmType}) into its closed form,
     * represented as a {@link ClosedValueType}. Any type indices in the type are resolved using the
     * provided symbol table.
     * <p>
     * It is legal to call this function with a null {@code symbolTable}. This is used in cases
     * where we need to map a predefined value type to the closed type data representation (i.e. we
     * know the type is already closed anyway and so it does not contain any type indices).
     * </p>
     *
     * @param type the {@code int}-encoded Wasm type to be expanded
     * @param symbolTable used for lookup of type definitions when expanding type indices
     */
    public static ClosedValueType closedTypeOf(int type, SymbolTable symbolTable) {
        return switch (type) {
            case WasmType.I32_TYPE -> NumberType.I32;
            case WasmType.I64_TYPE -> NumberType.I64;
            case WasmType.F32_TYPE -> NumberType.F32;
            case WasmType.F64_TYPE -> NumberType.F64;
            case WasmType.V128_TYPE -> VectorType.V128;
            default -> {
                assert WasmType.isReferenceType(type);
                boolean nullable = WasmType.isNullable(type);
                yield switch (WasmType.getAbstractHeapType(type)) {
                    case WasmType.FUNC_HEAPTYPE -> nullable ? ClosedReferenceType.FUNCREF : ClosedReferenceType.NONNULL_FUNCREF;
                    case WasmType.EXTERN_HEAPTYPE -> nullable ? ClosedReferenceType.EXTERNREF : ClosedReferenceType.NONNULL_EXTERNREF;
                    case WasmType.EXN_HEAPTYPE -> nullable ? ClosedReferenceType.EXNREF : ClosedReferenceType.NONNULL_EXNREF;
                    default -> {
                        assert WasmType.isConcreteReferenceType(type);
                        assert symbolTable != null;
                        int typeIndex = WasmType.getTypeIndex(type);
                        ClosedHeapType heapType = symbolTable.closedTypeAt(typeIndex);
                        yield new ClosedReferenceType(nullable, heapType);
                    }
                };
            }
        };
    }

    /**
     * Checks whether the type {@code actualType} matches the type {@code expectedType}. This is the
     * case when {@code actualType} is a subtype of {@code expectedType}.
     */
    public boolean matchesType(int expectedType, int actualType) {
        switch (expectedType) {
            case WasmType.BOT -> {
                return false;
            }
            case WasmType.TOP -> {
                return true;
            }
        }
        switch (actualType) {
            case WasmType.BOT -> {
                return true;
            }
            case WasmType.TOP -> {
                return false;
            }
        }
        return closedTypeOf(expectedType).isSupertypeOf(closedTypeOf(actualType));
    }

    public void importSymbol(ImportDescriptor descriptor) {
        checkNotParsed();
        assert importedSymbols.size() == descriptor.importedSymbolIndex();
        importedSymbols.add(descriptor);
    }

    public List<ImportDescriptor> importedSymbols() {
        return importedSymbols;
    }

    public int numImportedSymbols() {
        return importedSymbols.size();
    }

    protected void exportSymbol(String name) {
        checkNotParsed();
        checkUniqueExport(name);
        exportedSymbols.add(name);
    }

    public List<String> exportedSymbols() {
        return exportedSymbols;
    }

    public void exportFunction(int functionIndex, String exportName) {
        checkNotParsed();
        exportSymbol(exportName);
        exportedFunctions.put(exportName, functions[functionIndex]);
        exportedFunctionsByIndex.put(functionIndex, exportName);
        module().addLinkAction((context, store, instance, imports) -> {
            store.linker().resolveFunctionExport(module(), functionIndex, exportName);
        });
    }

    public EconomicMap<String, WasmFunction> exportedFunctions() {
        return exportedFunctions;
    }

    public WasmFunction importFunction(String moduleName, String functionName, int typeIndex) {
        checkNotParsed();
        final ImportDescriptor descriptor = new ImportDescriptor(moduleName, functionName, ImportIdentifier.FUNCTION, numFunctions, numImportedSymbols());
        importSymbol(descriptor);
        WasmFunction function = allocateFunction(typeIndex, descriptor);
        assert function.index() == descriptor.targetIndex();
        importedFunctions.add(function);
        numImportedFunctions++;
        module().addLinkAction((context, store, instance, imports) -> {
            store.linker().resolveFunctionImport(store, instance, function, imports);
        });
        return function;
    }

    public List<WasmFunction> importedFunctions() {
        return importedFunctions;
    }

    public int numImportedFunctions() {
        return numImportedFunctions;
    }

    public WasmFunction importedFunction(ImportDescriptor descriptor) {
        return functions[descriptor.targetIndex()];
    }

    private void ensureGlobalsCapacity(int index) {
        while (index >= globalInitializers.length) {
            final int[] nGlobalTypes = new int[globalTypes.length * 2];
            final byte[] nGlobalFlags = new byte[globalFlags.length * 2];
            final Object[] nGlobalInitializers = new Object[globalInitializers.length * 2];
            System.arraycopy(globalTypes, 0, nGlobalTypes, 0, globalTypes.length);
            System.arraycopy(globalFlags, 0, nGlobalFlags, 0, globalFlags.length);
            System.arraycopy(globalInitializers, 0, nGlobalInitializers, 0, globalInitializers.length);
            globalTypes = nGlobalTypes;
            globalFlags = nGlobalFlags;
            globalInitializers = nGlobalInitializers;
        }
    }

    private void ensureGlobalInitializersBytecodeCapacity(int index) {
        while (index >= globalInitializersBytecode.length) {
            final byte[][] nGlobalInitializersBytecode = new byte[globalInitializersBytecode.length * 2][];
            System.arraycopy(globalInitializersBytecode, 0, nGlobalInitializersBytecode, 0, globalInitializersBytecode.length);
            globalInitializersBytecode = nGlobalInitializersBytecode;
        }
    }

    /**
     * Allocates a global index in the symbol table, for a global variable that was already
     * allocated.
     */
    void allocateGlobal(int index, int valueType, byte mutability, boolean initialized, boolean imported, byte[] initBytecode, Object initialValue) {
        checkNotParsed();
        ensureGlobalsCapacity(index);
        numGlobals = maxUnsigned(index + 1, numGlobals);
        byte flags;
        if (mutability == GlobalModifier.CONSTANT) {
            flags = 0;
        } else if (mutability == GlobalModifier.MUTABLE) {
            flags = GLOBAL_MUTABLE_BIT;
        } else {
            throw WasmException.create(Failure.UNSPECIFIED_INVALID, "Invalid mutability: " + mutability);
        }
        if (initialized) {
            flags |= GLOBAL_INITIALIZED_BIT;
        }
        if (imported) {
            flags |= GLOBAL_IMPORTED_BIT;
            numExternalGlobals++;
        }
        if (initBytecode == null) {
            flags |= GLOBAL_FUNCTION_INITIALIZER_BIT;
            globalInitializers[index] = initialValue;
        } else {
            int initBytecodeIndex = numGlobalInitializersBytecode++;
            ensureGlobalInitializersBytecodeCapacity(initBytecodeIndex);
            globalInitializersBytecode[initBytecodeIndex] = initBytecode;
            globalInitializers[index] = initBytecodeIndex;
        }
        globalTypes[index] = valueType;
        globalFlags[index] = flags;
    }

    /**
     * Declares a non-imported global defined in this module. The global will be internal by
     * default, but may be exported using {@link #exportGlobal}. Imported globals are declared using
     * {@link #importGlobal} instead. This method may only be called during parsing, before linking.
     */
    void declareGlobal(int index, int valueType, byte mutability, boolean initialized, byte[] initBytecode, Object initialValue) {
        assert initialized == (initBytecode == null) : index;
        allocateGlobal(index, valueType, mutability, initialized, false, initBytecode, initialValue);
        module().addLinkAction((context, store, instance, imports) -> {
            store.linker().resolveGlobalInitialization(instance, index, initBytecode, initialValue);
        });
    }

    /**
     * Declares an imported global. May be re-exported.
     */
    void importGlobal(String moduleName, String globalName, int index, int valueType, byte mutability) {
        final ImportDescriptor descriptor = new ImportDescriptor(moduleName, globalName, ImportIdentifier.GLOBAL, index, numImportedSymbols());
        importedGlobals.put(index, descriptor);
        importSymbol(descriptor);
        allocateGlobal(index, valueType, mutability, false, true, null, null);
        module().addLinkAction((context, store, instance, imports) -> {
            store.linker().resolveGlobalImport(store, instance, descriptor, index, valueType, mutability, imports);
        });
    }

    public EconomicMap<Integer, ImportDescriptor> importedGlobals() {
        return importedGlobals;
    }

    public EconomicMap<ImportDescriptor, Integer> importedGlobalDescriptors() {
        final EconomicMap<ImportDescriptor, Integer> reverseMap = EconomicMap.create();
        MapCursor<Integer, ImportDescriptor> cursor = importedGlobals.getEntries();
        while (cursor.advance()) {
            reverseMap.put(cursor.getValue(), cursor.getKey());
        }
        return reverseMap;
    }

    public int numGlobals() {
        return numGlobals;
    }

    public int numInternalGlobals() {
        return numGlobals - numExternalGlobals;
    }

    public int numExternalGlobals() {
        return numExternalGlobals;
    }

    public final int globalAddress(int index) {
        return globalAddresses[index];
    }

    public byte globalMutability(int index) {
        if ((globalFlags(index) & GLOBAL_MUTABLE_BIT) != 0) {
            return GlobalModifier.MUTABLE;
        } else {
            return GlobalModifier.CONSTANT;
        }
    }

    public boolean isGlobalMutable(int index) {
        return globalMutability(index) == GlobalModifier.MUTABLE;
    }

    public int globalValueType(int index) {
        return globalTypes[index];
    }

    private byte globalFlags(int index) {
        return globalFlags[index];
    }

    public boolean globalInitialized(int index) {
        return (globalFlags(index) & GLOBAL_INITIALIZED_BIT) != 0;
    }

    public byte[] globalInitializerBytecode(int index) {
        if ((globalFlags(index) & GLOBAL_FUNCTION_INITIALIZER_BIT) != 0) {
            return null;
        } else {
            return globalInitializersBytecode[(int) globalInitializers[index]];
        }
    }

    public Object globalInitialValue(int index) {
        if ((globalFlags(index) & GLOBAL_FUNCTION_INITIALIZER_BIT) != 0) {
            return globalInitializers[index];
        } else {
            return 0;
        }
    }

    public boolean globalImported(int index) {
        return (globalFlags(index) & GLOBAL_IMPORTED_BIT) != 0;
    }

    public boolean globalExported(int index) {
        return (globalFlags(index) & GLOBAL_EXPORTED_BIT) != 0;
    }

    public boolean globalExternal(int index) {
        return (globalFlags(index) & (GLOBAL_IMPORTED_BIT | GLOBAL_EXPORTED_BIT)) != 0;
    }

    public EconomicMap<String, Integer> exportedGlobals() {
        return exportedGlobals;
    }

    void exportGlobal(String name, int index) {
        checkNotParsed();
        exportSymbol(name);
        if (!globalExternal(index)) {
            numExternalGlobals++;
        }
        globalFlags[index] |= GLOBAL_EXPORTED_BIT;
        exportedGlobals.put(name, index);
        module().addLinkAction((context, store, instance, imports) -> {
            store.linker().resolveGlobalExport(instance.module(), name, index);
        });
    }

    public void declareExportedGlobalWithValue(String name, int index, int valueType, byte mutability, Object value) {
        checkNotParsed();
        declareGlobal(index, valueType, mutability, true, null, value);
        exportGlobal(name, index);
    }

    private void ensureTableCapacity(int index) {
        if (index >= tables.length) {
            final TableInfo[] nTables = new TableInfo[Math.max(Integer.highestOneBit(index) << 1, 2 * tables.length)];
            System.arraycopy(tables, 0, nTables, 0, tables.length);
            tables = nTables;
        }
    }

    public void declareTable(int index, int declaredMinSize, int declaredMaxSize, int elemType, byte[] initBytecode, Object initValue, boolean referenceTypes) {
        checkNotParsed();
        addTable(index, declaredMinSize, declaredMaxSize, elemType, initValue, initBytecode, referenceTypes);
        module().addLinkAction((context, store, instance, imports) -> {
            final int maxAllowedSize = minUnsigned(declaredMaxSize, module().limits().tableInstanceSizeLimit());
            module().limits().checkTableInstanceSize(declaredMinSize);
            final WasmTable wasmTable;
            if (context.getContextOptions().memoryOverheadMode()) {
                // Initialize an empty table in memory overhead mode.
                wasmTable = new WasmTable(0, 0, 0, elemType, this);
            } else {
                wasmTable = new WasmTable(declaredMinSize, declaredMaxSize, maxAllowedSize, elemType, this);
            }
            instance.setTable(index, wasmTable);

            store.linker().resolveTableInitialization(instance, index, initBytecode, initValue);
        });
    }

    void importTable(String moduleName, String tableName, int index, int initSize, int maxSize, int elemType, boolean referenceTypes) {
        checkNotParsed();
        addTable(index, initSize, maxSize, elemType, null, null, referenceTypes);
        final ImportDescriptor importedTable = new ImportDescriptor(moduleName, tableName, ImportIdentifier.TABLE, index, numImportedSymbols());
        importedTables.put(index, importedTable);
        importSymbol(importedTable);
        module().addLinkAction((context, store, instance, imports) -> {
            instance.setTable(index, null);
            store.linker().resolveTableImport(store, instance, importedTable, index, initSize, maxSize, elemType, imports);
        });
    }

    void addTable(int index, int minSize, int maxSize, int elemType, Object initValue, byte[] initBytecode, boolean referenceTypes) {
        if (!referenceTypes) {
            assertTrue(importedTables.isEmpty(), "A table has already been imported in the module.", Failure.MULTIPLE_TABLES);
            assertTrue(tableCount == 0, "A table has already been declared in the module.", Failure.MULTIPLE_TABLES);
        }
        ensureTableCapacity(index);
        final TableInfo table = new TableInfo(minSize, maxSize, elemType, initValue, initBytecode);
        tables[index] = table;
        tableCount++;
    }

    boolean checkTableIndex(int tableIndex) {
        return Integer.compareUnsigned(tableIndex, tableCount) < 0;
    }

    public void exportTable(int tableIndex, String name) {
        checkNotParsed();
        exportSymbol(name);
        if (!checkTableIndex(tableIndex)) {
            throw WasmException.create(Failure.UNSPECIFIED_INVALID, "No table has been declared or imported, so a table cannot be exported.");
        }
        exportedTables.put(name, tableIndex);
        module().addLinkAction((context, store, instance, imports) -> {
            store.linker().resolveTableExport(module(), tableIndex, name);
        });
    }

    public int tableCount() {
        return tableCount;
    }

    public ImportDescriptor importedTable(int index) {
        return importedTables.get(index);
    }

    public EconomicMap<ImportDescriptor, Integer> importedTableDescriptors() {
        final EconomicMap<ImportDescriptor, Integer> reverseMap = EconomicMap.create();
        MapCursor<Integer, ImportDescriptor> cursor = importedTables.getEntries();
        while (cursor.advance()) {
            reverseMap.put(cursor.getValue(), cursor.getKey());
        }
        return reverseMap;
    }

    public EconomicMap<String, Integer> exportedTables() {
        return exportedTables;
    }

    public int tableInitialSize(int index) {
        final TableInfo table = tables[index];
        assert table != null;
        return table.initialSize;
    }

    public int tableMaximumSize(int index) {
        final TableInfo table = tables[index];
        assert table != null;
        return table.maximumSize;
    }

    public int tableElementType(int index) {
        final TableInfo table = tables[index];
        assert table != null;
        return table.elemType;
    }

    public Object tableInitialValue(int index) {
        final TableInfo table = tables[index];
        assert table != null;
        return table.initValue;
    }

    public byte[] tableInitializerBytecode(int index) {
        final TableInfo table = tables[index];
        assert table != null;
        return table.initBytecode;
    }

    private void ensureMemoryCapacity(int index) {
        if (index >= memories.length) {
            final MemoryInfo[] nMemories = new MemoryInfo[Math.max(Integer.highestOneBit(index) << 1, 2 * memories.length)];
            System.arraycopy(memories, 0, nMemories, 0, memories.length);
            memories = nMemories;
        }
    }

    public void allocateMemory(int index, long declaredMinSize, long declaredMaxSize, boolean indexType64, boolean shared, boolean multiMemory, boolean useUnsafeMemory,
                    boolean directByteBufferMemoryAccess) {
        checkNotParsed();
        addMemory(index, declaredMinSize, declaredMaxSize, indexType64, shared, multiMemory);
        module().addLinkAction((context, store, instance, imports) -> {
            module().limits().checkMemoryInstanceSize(declaredMinSize, indexType64);
            final WasmMemory wasmMemory;
            if (context.getContextOptions().memoryOverheadMode()) {
                // Initialize an empty memory when in memory overhead mode.
                wasmMemory = WasmMemoryFactory.createMemory(0, 0, false, false, useUnsafeMemory, directByteBufferMemoryAccess, context);
            } else {
                wasmMemory = WasmMemoryFactory.createMemory(declaredMinSize, declaredMaxSize, indexType64, shared, useUnsafeMemory, directByteBufferMemoryAccess, context);
            }
            instance.setMemory(index, wasmMemory);
        });
    }

    public void importMemory(String moduleName, String memoryName, int index, long initSize, long maxSize, boolean typeIndex64, boolean shared, boolean multiMemory) {
        checkNotParsed();
        addMemory(index, initSize, maxSize, typeIndex64, shared, multiMemory);
        final ImportDescriptor importedMemory = new ImportDescriptor(moduleName, memoryName, ImportIdentifier.MEMORY, index, numImportedSymbols());
        importedMemories.put(index, importedMemory);
        importSymbol(importedMemory);
        module().addLinkAction((context, store, instance, imports) -> {
            store.linker().resolveMemoryImport(store, instance, importedMemory, index, initSize, maxSize, typeIndex64, shared, imports);
        });
    }

    void addMemory(int index, long minSize, long maxSize, boolean indexType64, boolean shared, boolean multiMemory) {
        if (!multiMemory) {
            assertTrue(importedMemories.isEmpty(), "A memory has already been imported in the module.", Failure.MULTIPLE_MEMORIES);
            assertTrue(memoryCount == 0, "A memory has already been declared in the module.", Failure.MULTIPLE_MEMORIES);
        }
        ensureMemoryCapacity(index);
        final MemoryInfo memory = new MemoryInfo(minSize, maxSize, indexType64, shared);
        memories[index] = memory;
        memoryCount++;
    }

    boolean checkMemoryIndex(int memoryIndex) {
        return Integer.compareUnsigned(memoryIndex, memoryCount) < 0;
    }

    public void exportMemory(int memoryIndex, String name) {
        checkNotParsed();
        exportSymbol(name);
        if (!checkMemoryIndex(memoryIndex)) {
            throw WasmException.create(Failure.UNSPECIFIED_INVALID, "No memory with the specified index has been declared or imported, so it cannot be exported.");
        }
        exportedMemories.put(name, memoryIndex);
        module().addLinkAction((context, store, instance, imports) -> {
            store.linker().resolveMemoryExport(instance, memoryIndex, name);
        });
    }

    public int memoryCount() {
        return memoryCount;
    }

    public ImportDescriptor importedMemory(int index) {
        return importedMemories.get(index);
    }

    public EconomicMap<ImportDescriptor, Integer> importedMemoryDescriptors() {
        final EconomicMap<ImportDescriptor, Integer> reverseMap = EconomicMap.create();
        MapCursor<Integer, ImportDescriptor> cursor = importedMemories.getEntries();
        while (cursor.advance()) {
            reverseMap.put(cursor.getValue(), cursor.getKey());
        }
        return reverseMap;
    }

    public EconomicMap<String, Integer> exportedMemories() {
        return exportedMemories;
    }

    public long memoryInitialSize(int index) {
        final MemoryInfo memory = memories[index];
        return memory.initialSize;
    }

    public long memoryMaximumSize(int index) {
        final MemoryInfo memory = memories[index];
        return memory.maximumSize;
    }

    public boolean memoryHasIndexType64(int index) {
        final MemoryInfo memory = memories[index];
        return memory.indexType64;
    }

    public boolean memoryIsShared(int index) {
        final MemoryInfo memory = memories[index];
        return memory.shared;
    }

    private void ensureTagCapacity(int index) {
        if (index >= tags.length) {
            final TagInfo[] nTags = new TagInfo[Math.max(Integer.highestOneBit(index) << 1, 2 * tags.length)];
            System.arraycopy(tags, 0, nTags, 0, tags.length);
            tags = nTags;
        }
    }

    public void allocateTag(int index, byte attribute, int typeIndex) {
        checkNotParsed();
        addTag(index, attribute, typeIndex);
        module().addLinkAction((context, store, instance, imports) -> {
            final WasmTag tag = new WasmTag(closedFunctionTypeAt(typeIndex));
            instance.setTag(index, tag);
        });
    }

    public void importTag(String moduleName, String tagName, int index, byte attribute, int typeIndex) {
        checkNotParsed();
        addTag(index, attribute, typeIndex);
        final ImportDescriptor importedTag = new ImportDescriptor(moduleName, tagName, ImportIdentifier.TAG, index, numImportedSymbols());
        final ClosedFunctionType type = closedFunctionTypeAt(typeIndex);
        importedTags.put(index, importedTag);
        importSymbol(importedTag);
        module().addLinkAction((context, store, instance, imports) -> {
            store.linker().resolveTagImport(store, instance, importedTag, index, type, imports);
        });
    }

    void addTag(int index, byte attribute, int typeIndex) {
        assertIntEqual(functionTypeResultCount(typeIndex), 0, Failure.NON_EMPTY_TAG_RESULT_TYPE);
        ensureTagCapacity(index);
        final TagInfo tag = new TagInfo(attribute, typeIndex);
        tags[index] = tag;
        tagCount++;
    }

    public void checkTagIndex(int tagIndex) {
        assertUnsignedIntLess(tagIndex, tagCount, Failure.UNKNOWN_TAG);
    }

    public void exportTag(int tagIndex, String name) {
        checkNotParsed();
        exportSymbol(name);
        if (!checkExistingTagIndex(tagIndex)) {
            throw WasmException.create(Failure.UNKNOWN_TAG, "No tag with the specified index has been declared or imported, so it cannot be exported.");
        }
        exportedTags.put(name, tagIndex);
        module().addLinkAction((context, store, instance, imports) -> {
            store.linker().resolveTagExport(instance, tagIndex, name);
        });
    }

    public int tagCount() {
        return tagCount;
    }

    public ImportDescriptor importedTag(int index) {
        return importedTags.get(index);
    }

    public EconomicMap<ImportDescriptor, Integer> importTagDescriptors() {
        final EconomicMap<ImportDescriptor, Integer> reverseMap = EconomicMap.create();
        final MapCursor<Integer, ImportDescriptor> cursor = importedTags.getEntries();
        while (cursor.advance()) {
            reverseMap.put(cursor.getValue(), cursor.getKey());
        }
        return reverseMap;
    }

    public EconomicMap<String, Integer> exportedTags() {
        return exportedTags;
    }

    private boolean checkExistingTagIndex(int index) {
        return Integer.compareUnsigned(index, tagCount) < 0;
    }

    public int tagTypeIndex(int index) {
        assert index < tags.length;
        return tags[index].typeIndex();
    }

    public byte tagAttribute(int index) {
        assert index < tags.length;
        return tags[index].attribute();
    }

    void allocateCustomSection(String name, int offset, int length) {
        customSections.add(new WasmCustomSection(name, offset, length));
    }

    public List<WasmCustomSection> customSections() {
        return customSections;
    }

    public void checkDataSegmentIndex(int dataIndex) {
        assertTrue(dataCountExists, Failure.DATA_COUNT_SECTION_REQUIRED);
        assertUnsignedIntLess(dataIndex, dataSegmentCount, Failure.UNKNOWN_DATA_SEGMENT);
    }

    public void setDataSegmentCount(int count) {
        this.dataSegmentCount = count;
        this.dataCountExists = true;
    }

    /**
     * Checks whether the actual number of data segments corresponds with the number defined in the
     * data count section.
     */
    public void checkDataSegmentCount(int numberOfDataSegments) {
        if (dataCountExists) {
            assertIntEqual(numberOfDataSegments, this.dataSegmentCount, Failure.DATA_COUNT_MISMATCH);
        }
    }

    public void addFunctionReference(int functionIndex) {
        functionReferences.add(functionIndex);
    }

    public void checkFunctionReference(int functionIndex) {
        assertTrue(functionReferences.contains(functionIndex), Failure.UNDECLARED_FUNCTION_REFERENCE);
    }

    private void ensureDataInstanceCapacity(int index) {
        if (dataInstances == null) {
            dataInstances = new int[Math.max(Integer.highestOneBit(index) << 1, 2)];
        } else if (index >= dataInstances.length) {
            final int[] nDataInstances = new int[Math.max(Integer.highestOneBit(index) << 1, 2 * dataInstances.length)];
            System.arraycopy(dataInstances, 0, nDataInstances, 0, dataInstances.length);
            dataInstances = nDataInstances;
        }
    }

    void setDataInstance(int index, int offset) {
        ensureDataInstanceCapacity(index);
        dataInstances[index] = offset;
        if (!dataCountExists) {
            dataSegmentCount++;
        }
    }

    public int dataInstanceOffset(int index) {
        return dataInstances[index];
    }

    public int dataInstanceCount() {
        return dataSegmentCount;
    }

    void setDroppedDataInstanceOffset(int address) {
        droppedDataInstanceOffset = address;
    }

    public int droppedDataInstanceOffset() {
        return droppedDataInstanceOffset;
    }

    public void checkElemIndex(int elemIndex) {
        assertUnsignedIntLess(elemIndex, elemSegmentCount, Failure.UNKNOWN_ELEM_SEGMENT);
    }

    public void checkElemType(int elemIndex, int expectedType) {
        Assert.assertTrue(matchesType(expectedType, (int) elemInstances[elemIndex]), Failure.TYPE_MISMATCH);
    }

    private void ensureElemInstanceCapacity(int index) {
        if (elemInstances == null) {
            elemInstances = new long[Math.max(Integer.highestOneBit(index) << 1, 2)];
        } else if (index >= elemInstances.length) {
            final long[] nElementInstances = new long[Math.max(Integer.highestOneBit(index) << 1, 2 * elemInstances.length)];
            System.arraycopy(elemInstances, 0, nElementInstances, 0, elemInstances.length);
            elemInstances = nElementInstances;
        }
    }

    void setElemInstance(int index, int offset, int elemType) {
        ensureElemInstanceCapacity(index);
        elemInstances[index] = (long) offset << 32 | (elemType & 0xFFFF_FFFFL);
        elemSegmentCount++;
    }

    public int elemInstanceOffset(int index) {
        return (int) (elemInstances[index] >>> 32);
    }

    public int elemInstanceCount() {
        return elemSegmentCount;
    }

    private void ensureCodeEntriesCapacity(int index) {
        if (codeEntries == null) {
            codeEntries = new int[Math.max(Integer.highestOneBit(index) << 1, 2)];
        } else if (index >= codeEntries.length) {
            final int[] nCodeEntries = new int[Math.max(Integer.highestOneBit(index) << 1, 2 * codeEntries.length)];
            System.arraycopy(codeEntries, 0, nCodeEntries, 0, codeEntries.length);
            codeEntries = nCodeEntries;
        }
    }

    void setCodeEntryOffset(int index, int offset) {
        ensureCodeEntriesCapacity(index);
        codeEntries[index] = offset;
        codeEntryCount++;
    }

    public int codeEntryOffset(int index) {
        return codeEntries[index];
    }

    public int codeEntryCount() {
        return codeEntryCount;
    }

    /**
     * Assigns global addresses and trims symbol table after parsing.
     */
    public void finishSymbolTable() {
        CompilerAsserts.neverPartOfCompilation();
        assignGlobalAddresses();
        removeFunctionReferences();
    }

    private void assignGlobalAddresses() {
        CompilerAsserts.neverPartOfCompilation();
        assert numGlobals() == numInternalGlobals() + numExternalGlobals();
        this.globalAddresses = new int[numGlobals];
        int internalGlobalCount = 0;
        int externalGlobalCount = 0;
        for (int i = 0; i < numGlobals; i++) {
            if (!globalExternal(i)) {
                globalAddresses[i] = internalGlobalCount;
                internalGlobalCount++;
            } else {
                globalAddresses[i] = -externalGlobalCount - 1;
                externalGlobalCount++;
            }
        }
        assert internalGlobalCount == numInternalGlobals();
        assert externalGlobalCount == numExternalGlobals();
    }

    private void removeFunctionReferences() {
        CompilerAsserts.neverPartOfCompilation();
        functionReferences = null;
    }
}
